// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @type {{
 *  CONTENT_CATEGORY: string,
 *  AppendChild: !function(!Node, !Node),
 *  InsertBefore: !function(!Node, !Node, ?Node),
 *  Editor: !function(!Document),
 *  RemoveAttribute: !function(!Node, string),
 *  RemoveChild: !function(!Node, !Node),
 *  ReplaceChild: !function(!Node, !Node, !Node),
 *  SetAttribute: !function(!Node, string),
 *  SetStyle: !function(!Node, string, string),
 *  SplitText: !function(!Text, number),
 *  contentModel: !Object.<string, !ContentModel>,
 *  define: function(string, !function()),
 *  defineCommand: function(string, !function()),
 *  isContentEditable: function(!Node) : boolean,
 *  lookupCommand: function(string) : ?function(),
 *  newSet: function(!Array) : !Set,
 *  nodes: {
 *    isDescendantOf: function(!Node, !Node) : boolean,
 *    isElement: function(!Node) : boolean,
 *    isText: function(!Node) : boolean,
 *    maxOffset: function(!Node) : number,
 *    nextNodeSkippingChildren: function(?Node) : ?Node,
 *    nodeIndex: function(!Node): number
 *  }
 * }}
 */
var editing = {};

(function() {
  /** @type {!Map.<string, !CommandFunction>} */
  var commandTable = new Map();

  /**
   * @param {string} name
   * @param {*} value
   */
  function define(name, value) {
    Object.defineProperty(editing, name, {value: value});
  }

  /**
   * @param {string} name
   * @param {!CommandFunction} commandFunction
   */
  function defineCommand(name, commandFunction) {
    var canonicalName = name.toLowerCase();
    commandTable.set(canonicalName, commandFunction);
    if (canonicalName == 'backcolor')
      defineCommand('hilitecolor', commandFunction);
  }

  // TODO(yosin) Once, Node.isContentEditable works for nodes without render
  // object, we dont' need to have |isContentEditablePollyfill|.
  // http://crbug.com/313082
  /**
   * @param {!Node} node
   * @return {boolean}
   */
  function isContentEditable(node) {
    if (window.document === node.ownerDocument &&
        node.style.display != 'none') {
      return node.isContentEditable;
    }
    if (node.isContentEditable)
      return true;
    if (node.nodeType != Node.ELEMENT_NODE)
      return false;
    var contentEditable = node.getAttribute('contenteditable');
    if (typeof(contentEditable) != 'string')
      return false;
    return contentEditable.toLowerCase() != 'false';
  }

  /**
   * @param {string} name
   * @return {?CommandFunction}
   */
  function lookupCommand(name) {
    return commandTable.get(name.toLowerCase()) || null;
  }

  /**
   * @param {!Array.<string>} members
   * @return {!Set.<string>}
   * IE11 doesn't support |new Set(Array)|.
   */
  function newSet(members) {
    var set = new Set(members);
    if (!set.size) {
      members.forEach(function(member) {
        set.add(member);
      });
    }
    return set;
  }

  Object.defineProperties(editing, {
    define: {value: define},
    defineCommand: {value: defineCommand},
    isContentEditable: {value: isContentEditable},
    lookupCommand: {value: lookupCommand},
    newSet: {value: newSet}
  });
})();
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.



// Instead of |var editing = {}| to make closure compiler happy.
window.editing = {};

(function() {
  /** @type {!Map.<string, !CommandFunction>} */
  var commandTable = new Map();

  /**
   * @param {string} name
   * @param {*} value
   */
  function define(name, value) {
    Object.defineProperty(editing, name, {value: value});
  }

  /**
   * @param {string} name
   * @param {!CommandFunction} commandFunction
   */
  function defineCommand(name, commandFunction) {
    var canonicalName = name.toLowerCase();
    commandTable.set(canonicalName, commandFunction);
    if (canonicalName == 'backcolor')
      defineCommand('hilitecolor', commandFunction);
  }

  // TODO(yosin) Once, Node.isContentEditable works for nodes without render
  // object, we dont' need to have |isContentEditablePollyfill|.
  // http://crbug.com/313082
  /**
   * @param {!Node} node
   * @return {boolean}
   */
  function isContentEditable(node) {
    if (window.document === node.ownerDocument &&
        node.style.display != 'none') {
      return node.isContentEditable;
    }
    if (node.isContentEditable)
      return true;
    if (node.nodeType != Node.ELEMENT_NODE)
      return false;
    var contentEditable = node.getAttribute('contenteditable');
    if (typeof(contentEditable) != 'string')
      return false;
    return contentEditable.toLowerCase() != 'false';
  }

  /**
   * @param {string} name
   * @return {?CommandFunction}
   */
  function lookupCommand(name) {
    return commandTable.get(name.toLowerCase()) || null;
  }

  /**
   * @param {!Array.<string>} members
   * @return {!Set.<string>}
   * IE11 doesn't support |new Set(Array)|.
   */
  function newSet(members) {
    var set = new Set(members);
    if (!set.size) {
      members.forEach(function(member) {
        set.add(member);
      });
    }
    return set;
  }

  Object.defineProperties(editing, {
    define: {value: define},
    defineCommand: {value: defineCommand},
    isContentEditable: {value: isContentEditable},
    lookupCommand: {value: lookupCommand},
    newSet: {value: newSet}
  });
})();
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.



editing.define('nodes', (function() {
  /** @const */ var INTERACTIVE = editing.CONTENT_CATEGORY.INTERACTIVE;
  /** @const */ var PHRASING = editing.CONTENT_CATEGORY.PHRASING;

  /**
   * @constructor
   * @final
   * @param {?Node} startNode
   */
  function NextNodes(startNode) {
    this.currentNode_ = startNode;
    Object.seal(this);
  }

  /** @type {!function(): {done: boolean, value: !Node}} */
  NextNodes.prototype.next;

  Object.defineProperty(NextNodes.prototype, 'next', {
    value: function() {
      var resultNode = this.currentNode_;
      if (!resultNode)
        return {done: true};
      this.currentNode_ = nextNode(this.currentNode_);
      return {done: false, value: resultNode};
    }
  });

  /**
   * @param {!Node} node1
   * @param {!Node} node2
   * @return {?Node}
   */
  function commonAncestor(node1, node2) {
    console.assert(node1.ownerDocument === node2.ownerDocument);
    if (node1 === node2)
      return node1;
    var depth1 = 0;
    for (var node = node1; node; node = node.parentNode) {
      if (node == node2)
        return node;
      ++depth1;
    }
    var depth2 = 0;
    for (var node = node2; node; node = node.parentNode) {
      if (node == node1)
        return node;
      ++depth2;
    }
    var runner1 = node1;
    var runner2 = node2;
    if (depth1 > depth2) {
      for (var depth  = depth1; depth > depth2; --depth) {
        runner1 = runner1.parentNode;
      }
    } else if (depth2 > depth1) {
      for (var depth  = depth2; depth > depth1; --depth) {
        runner2 = runner2.parentNode;
      }
    }
    while (runner1) {
      if (runner1 == runner2)
        return runner1;
       runner1 = runner1.parentNode;
       runner2 = runner2.parentNode;
    }
    console.assert(!runner2);
    return null;
  }

  /**
   * @this {!ReadOnlySelection} selection
   * @return {!Array.<!Node>}
   *
   * Computes effective nodes for inline formatting commands. |selection|
   * should be normalized.
   */
  function computeEffectiveNodes(selection) {
    if (isText(selection.anchorNode) || isText(selection.focusNode))
      throw new Error('Selection should be normalized.');
    var selectedNodes = computeSelectedNodes(selection);
    if (!selectedNodes.length)
      return selectedNodes;
    var nodeSet = editing.newSet(selectedNodes);
    // Add ancestors of start node of selected nodes if all descendant nodes
    // in selected range, e.g. <a>^foo<b>bar</b>|</a>.
    // Note: selection doesn't need to end beyond end tag.
    var startNode = selectedNodes[0];
    for (var ancestor = startNode.parentNode; ancestor;
         ancestor = ancestor.parentNode) {
      if (!isEditable(ancestor))
        break;
      if (ancestor.firstChild !== startNode)
        break;
      if (!nodeSet.has(lastWithIn(ancestor)))
        break;
      selectedNodes.unshift(ancestor);
      nodeSet.add(ancestor);
      startNode = ancestor;
    }
    return selectedNodes;
  }

  /**
   * @param {!ReadOnlySelection} selection
   * @return {!Array.<!Node>}
   *
   * Note: When selection range has no node, e.g. <p><a>foo^</a>|</p>; enclosing
   * end tag, return value is empty array.
   */
  function computeSelectedNodes(selection) {
    if (selection.isEmpty)
      return [];

    var startContainer = /** @type {!Node} */(selection.startContainer);
    var startOffset = selection.startOffset;
    if (isText(startContainer)) {
      startContainer = startContainer.parentNode;
      startOffset = 0;
    }

    var endContainer = /** @type {!Node} */(selection.endContainer);
    var endOffset = selection.endOffset;
    if (isText(endContainer)) {
      endContainer = endContainer.parentNode;
      endOffset = 0;
    }

    /** @const @type {?Node} */
    var startNode = startContainer.childNodes[startOffset] ||
                    nextNode(selection.startContainer.lastChild);
    /** @const @type {?Node} */
    var endNode = endContainer.childNodes[endOffset] ||
                  nextNodeSkippingChildren(endContainer.lastChild);

    // Both, |startNode| and |endNode| are nullable, e.g. <a><b>abcd|</b></a>
    if (!startNode)
      return [];

    var nodes = [];
    var iterator = nextNodes(startNode);
    var current;
    while (!(current = iterator.next()).done) {
      if (current.value === endNode)
        break;
      if (current.value == endContainer && !selection.endOffset)
        break;
      nodes.push(current.value);
    }
    return nodes;
  }

  /**
   * @param {!Element} element
   * @return {boolean}
   */
  function isContentEditable(element) {
    for (var runner = element; runner && editing.nodes.isElement(runner);
         runner = runner.parentNode) {
      var contentEditable = runner.getAttribute('contenteditable');
      if (typeof(contentEditable) == 'string')
        return contentEditable.toLowerCase() != 'false';
      if (editing.isContentEditable(runner))
        return true;
    }
    return false;
  }

  /**
   * @param {!Node} node
   * @param {!Node} other
   * Returns true if |other| is an ancestor of |node|, otherwise false.
   */
  function isDescendantOf(node, other) {
    console.assert(other);
    for (var runner = node.parentNode; runner; runner = runner.parentNode) {
      if (runner == other)
        return true;
    }
    return false;
  }

  /**
   * @param {!Node} node
   * @return {boolean}
   */
  function isEditable(node) {
    var container = node.parentNode;
    if (!container)
      return false;
    return editing.nodes.isContentEditable(container);
  }

  /**
   * @param {!Node} node
   * @return {boolean}
   */
  function isElement(node) {
    return node.nodeType == Node.ELEMENT_NODE;
  }

  /**
   * @param {!Node} node
   * @return {boolean}
   */
  function isInteractive(node) {
    var model = editing.contentModel[node.nodeName];
    return model !== undefined && Boolean(model.categories[INTERACTIVE]);
  }

  /**
   * @param {!Node} node
   * @return {boolean}
   */
  function isPhrasing(node) {
    if (!editing.nodes.isElement(node))
      return true;
    var model = editing.contentModel[node.nodeName];
    return model !== undefined && Boolean(model.categories[PHRASING]);
  }

  /**
   * @param {!Node} node
   * @return {boolean}
   */
  function isText(node) {
    return node.nodeType == Node.TEXT_NODE;
  }

  /**
   * @param {!Node} node
   * @return {boolean}
   */
  function isVisibleNode(node) {
    if (isWhitespaceNode(node))
      return false;
    if (node.nodeType != Node.ELEMENT_NODE)
      return Boolean(node.parentNode && isVisibleNode(node.parentNode));
    var element = /** @type {!Element} */(node);
    var style = window.getComputedStyle(element);
    if (style)
      return style.display != 'none';
    if (!node.parentNode)
      return node.nodeType == Node.TEXT_NODE;
    return isVisibleNode(node.parentNode);
  }

  /**
   * @param {!Node} node
   * @return {boolean}
   */
  function isWhitespaceNode(node) {
    if (!editing.nodes.isText(node))
      return false;
    var text = node.nodeValue.replace(/[ \t\r\n]/g, '');
    return text == '';
  }

  function lastWithIn(current) {
    var descendant = current.lastChild;
    for (var child = descendant; child; child = child.lastChild) {
      descendant = child;
    }
    return descendant;
  }

  /**
   * @param {!Node} node
   * @return {number}
   */
  function maxOffset(node) {
    return editing.nodes.isText(node) ? node.nodeValue.length :
                                        node.childNodes.length;
  }

  /**
   * @param {?Node} current
   * @return {?Node}
   * nextNode(<a><b>foo|</b><a>bar) = bar
   */
  function nextNode(current) {
    if (!current)
      return null;
    if (current.firstChild)
      return current.firstChild;
    if (current.nextSibling)
      return current.nextSibling;
    return nextAncestorOrSibling(current);
  }

  /**
   * @param {?Node} node
   * @return {!NextNodes}
   */
  function nextNodes(node) {
    return new NextNodes(node);
  }

  /**
   * @param {!Node} current
   * @return {?Node}
   */
  function nextAncestorOrSibling(current) {
    console.assert(!current.nextSibling);
    for (var parent = current.parentNode; parent; parent = parent.parentNode) {
      if (parent.nextSibling)
        return parent.nextSibling;
    }
    return null;
  }

  /**
   * @param {?Node} current
   * @return {?Node}
   */
  function nextNodeSkippingChildren(current) {
    if (!current)
      return null;
    if (current.nextSibling)
      return current.nextSibling;
    return nextAncestorOrSibling(current);
  }

  /**
   * @param {!Node} node
   * @return {number}
   */
  function nodeIndex(node) {
    var index = 0;
    var parentNode = node.parentNode;
    console.assert(parentNode, node);
    for (var child = parentNode.firstChild; child; child = child.nextSibling) {
      if (child === node)
        return index;
      ++index;
    }
    throw new Error('NOTREACEHD');
  }

  /**
   * @param {!EditingContext} context
   * @param {!ReadOnlySelection} selection
   */
  function normalizeSelection(context, selection) {
    if (selection.isEmpty)
      return selection;

    var anchorNode = selection.anchorNode;
    var anchorOffset = selection.anchorOffset;
    var focusNode = selection.focusNode;
    var focusOffset = selection.focusOffset;

    /**
     * @param {?Node} node
     * @param {number} offset
     */
    function splitIfNeeded(node, offset) {
      if (!node)
        return;
      if (!editing.nodes.isText(node) || !offset)
        return;
      var textNode = /** @type {!Text} */(node);
      var text = node.nodeValue;
      if (text.length == offset)
        return;
      if (!offset || offset >= text.length) {
        throw new Error('Offset ' + offset + ' must be grater than zero and ' +
                        'less than ' + text.length + ' for ' + node);
      }
      var newTextNode = context.splitText(textNode, offset);
      if (anchorNode === node && anchorOffset >= offset) {
        anchorNode = newTextNode;
        anchorOffset -= offset;
      }
      if (focusNode === node && focusOffset >= offset) {
        focusNode = newTextNode;
        focusOffset -= offset;
      }
    }

    /**
     * @param {?Node} node
     * @param {number} offset
     */
    function useContainerIfPossible(node, offset) {
      if (!node)
        return;
      if (!editing.nodes.isText(node))
        return;
      var container = node.parentNode;
      var offsetInContainer = editing.nodes.nodeIndex(node);
      if (anchorNode === node && anchorOffset == offset) {
        anchorNode = container;
        anchorOffset = offset ? offsetInContainer + 1 : offsetInContainer;
      }
      if (focusNode === node && focusOffset == offset) {
        focusNode = container;
        focusOffset = offset ? offsetInContainer + 1 : offsetInContainer;
      }
    }

    // Split text boundary point
    splitIfNeeded(anchorNode, anchorOffset);
    splitIfNeeded(focusNode, focusOffset);

    // Convert text node + offset to container node + offset.
    useContainerIfPossible(anchorNode, anchorOffset);
    useContainerIfPossible(focusNode, focusOffset);
    return new editing.ReadOnlySelection(anchorNode, anchorOffset,
                                         focusNode, focusOffset,
                                         selection.direction);
  }

  /**
   * @param {!Node} current
   * @return {?Node}
   */
  function previousNode(current) {
    var previous = current.previousSibling;
    if (!previous)
      return current.parentNode;
    var child;
    while (child = previous.lastChild) {
      previous = child;
    }
    return previous;
  }

  /**
   * @param {!Node} current
   * @return {?Node}
   */
  function previousNodeSkippingChildren(current) {
    if (current.previousSibling)
      return current.previousSibling;
    for (var parent = current.parentNode; parent; parent = parent.parentNode) {
      if (parent.previousSibling)
        return parent.previousSibling;
    }
    return null;
  }

  return Object.defineProperties({}, {
    commonAncestor: {value: commonAncestor},
    computeEffectiveNodes: {value: computeEffectiveNodes},
    computeSelectedNodes: {value: computeSelectedNodes},
    isContentEditable: {value: isContentEditable},
    isDescendantOf: {value: isDescendantOf},
    isEditable: {value: isEditable},
    isElement: {value: isElement},
    isInteractive: {value: isInteractive},
    isPhrasing: {value: isPhrasing},
    isText: {value: isText},
    isVisibleNode: {value: isVisibleNode},
    isWhitespaceNode: {value: isWhitespaceNode},
    lastWithIn: {value: lastWithIn},
    maxOffset: {value: maxOffset},
    nextNode: {value: nextNode},
    nextNodes: {value: nextNodes},
    nextAncestorOrSibling: {value: nextAncestorOrSibling},
    nextNodeSkippingChildren: {value: nextNodeSkippingChildren},
    nodeIndex: {value: nodeIndex},
    normalizeSelection: {value: normalizeSelection},
    previousNode: {value: previousNode},
    previousNodeSkippingChildren: {value: previousNodeSkippingChildren},
  });
})());
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file./*

//////////////////////////////////////////////////////////////////////
//
// Operation
//
editing.define('Operation', (function() {
  /**
   * @constructor
   * @param {string} operationName
   */
  function Operation(operationName) {
    this.operationName_ = operationName;
  }

  /**
   * @this {!Operation}
   */
  function redo() {
    throw new Error('Abstract method Operation.prototype.redo called', this);
  }

  /**
   * @this {!Operation}
   */
  function undo() {
    throw new Error('Abstract method Operation.prototype.undo called', this);
  }

  Object.defineProperties(Operation.prototype, {
    constructor: Operation,
    execute: {value: function() { this.redo(); }},
    operationName: {get: function() { return this.operationName_; }},
    operationName_: {writable: true},
    redo: {value: redo },
    undo: {value: undo }
  });

  return Operation;
})());

//////////////////////////////////////////////////////////////////////
//
// AppendChild
//
editing.define('AppendChild', (function() {
  /**
   * @constructor
   * @param {!Node} parentNode
   * @param {!Node} newChild
   */
  function AppendChild(parentNode, newChild) {
    console.assert(!newChild.parentNode);
    editing.Operation.call(this, 'appendChild');
    this.parentNode_ = parentNode;
    this.newChild_ = newChild;
    Object.seal(this);
  }

  /**
   * @this {!AppendChild}
   */
  function redo() {
    this.parentNode_.appendChild(this.newChild_);
  }

  /**
   * @this {!AppendChild}
   */
  function undo() {
    this.parentNode_.removeChild(this.newChild_);
  }

  AppendChild.prototype = Object.create(editing.Operation.prototype, {
    constructor: AppendChild,
    newChild_: {writable: true},
    parentNode_: {writable: true},
    redo: {value: redo},
    undo: {value: undo}
  });

  return AppendChild;
})());

//////////////////////////////////////////////////////////////////////
//
// InsertBefore
//
editing.define('InsertBefore', (function() {
  /**
   * @constructor
   * @final
   * @param {!Node} parentNode
   * @param {!Node} newChild
   * @param {?Node} refChild
   */
  function InsertBefore(parentNode, newChild, refChild) {
    console.assert(!newChild.parentNode);
    console.assert(parentNode === refChild.parentNode);
    editing.Operation.call(this, 'insertBefore');
    this.parentNode_ = parentNode;
    this.newChild_ = newChild;
    this.refChild_ = refChild;
    Object.seal(this);
  }

  /**
   * @this {!InsertBefore}
   */
  function redo() {
    this.parentNode_.insertBefore(this.newChild_, this.refChild_);
  }

  /**
   * @this {!InsertBefore}
   */
  function undo() {
    this.parentNode_.removeChild(this.newChild_);
  }

  InsertBefore.prototype = Object.create(editing.Operation.prototype, {
    constructor: InsertBefore,
    newChild_: {writable: true},
    parentNode_: {writable: true},
    refChild_: {writable: true},
    redo: {value: redo},
    undo: {value: undo}
  });

  return InsertBefore;
})());

//////////////////////////////////////////////////////////////////////
//
// RemoveAttribute
//
editing.define('RemoveAttribute', (function() {
  /**
   * @constructor
   * @final
   * @param {!Element} element
   * @param {string} attrName
   */
  function RemoveAttribute(element, attrName) {
    editing.Operation.call(this, 'removeAttribute');
    this.element_ = element;
    this.attrName_ = attrName;
    this.oldValue_ = element.getAttribute(attrName);
    if (this.oldValue_ === null)
      throw new Error('You can not remove non-existing attribute ' + attrName);
    Object.seal(this);
  }

  /**
   * @this {!RemoveAttribute}
   */
  function redo() {
    this.element_.removeAttribute(this.attrName_);
  }

  /**
   * @this {!RemoveAttribute}
   */
  function undo() {
    this.element_.setAttribute(this.attrName_, this.oldValue_);
  }

  RemoveAttribute.prototype = Object.create(editing.Operation.prototype, {
    constructor: RemoveAttribute,
    attrName_: {writable: true},
    element_: {writable: true},
    oldValue_: {writable: true},
    redo: {value: redo},
    undo: {value: undo}
  });

  return RemoveAttribute;
})());

//////////////////////////////////////////////////////////////////////
//
// RemoveChild
//
editing.define('RemoveChild', (function() {
  /**
   * @constructor
   * @final
   * @param {!Node} parentNode
   * @param {?Node} oldChild
   */
  function RemoveChild(parentNode, oldChild) {
    editing.Operation.call(this, 'removeChild');
    this.parentNode_ = parentNode;
    this.oldChild_ = oldChild;
    this.refChild_ = oldChild.nextSibling;
    Object.seal(this);
  }

  /**
   * @this {!RemoveChild}
   */
  function redo() {
    this.parentNode_.removeChild(this.oldChild_);
  }

  /**
   * @this {!RemoveChild}
   */
  function undo() {
    this.parentNode_.insertBefore(this.oldChild_, this.refChild_);
  }

  RemoveChild.prototype = Object.create(editing.Operation.prototype, {
    constructor: RemoveChild,
    oldChild_: {writable: true},
    parentNode_: {writable: true},
    refChild_: {writable: true},
    redo: {value: redo},
    undo: {value: undo}
  });

  return RemoveChild;
})());

//////////////////////////////////////////////////////////////////////
//
// ReplaceChild
//
editing.define('ReplaceChild', (function() {
  /**
   * @constructor
   * @final
   * @param {!Node} parentNode
   * @param {!Node} newChild
   * @param {?Node} oldChild
   */
  function ReplaceChild(parentNode, newChild, oldChild) {
    console.assert(!newChild.parentNode);
    console.assert(parentNode === oldChild.parentNode);
    editing.Operation.call(this, 'replaceChild');
    this.parentNode_ = parentNode;
    this.newChild_ = newChild;
    this.oldChild_ = oldChild;
    Object.seal(this);
  }

  /**
   * @this {!ReplaceChild}
   */
  function redo() {
    this.parentNode_.replaceChild(this.newChild_, this.oldChild_);
  }

  /**
   * @this {!ReplaceChild}
   */
  function undo() {
    this.parentNode_.replaceChild(this.oldChild_, this.newChild_);
  }

  ReplaceChild.prototype = Object.create(editing.Operation.prototype, {
    constructor: ReplaceChild,
    newChild_: {writable: true},
    oldChild_: {writable: true},
    parentNode_: {writable: true},
    redo: {value: redo},
    undo: {value: undo}
  });

  return ReplaceChild;
})());

//////////////////////////////////////////////////////////////////////
//
// SetAttribute
//
editing.define('SetAttribute', (function() {
  /**
   * @constructor
   * @final
   * @param {!Element} element
   * @param {string} attrName
   * @param {string} newValue
   */
  function SetAttribute(element, attrName, newValue) {
    if (newValue === null)
      throw new Error('You can not use null for attribute ' + attrName);
    editing.Operation.call(this, 'setAttribute');
    this.element_ = element;
    this.attrName_ = attrName;
    this.newValue_ = newValue;
    this.oldValue_ = element.getAttribute(attrName);
    Object.seal(this);
  }

  /**
   * @this {!SetAttribute}
   */
  function redo() {
    this.element_.setAttribute(this.attrName_, this.newValue_);
  }

  /**
   * @this {!SetAttribute}
   */
  function undo() {
    if (this.oldValue_ === null)
      this.element_.removeAttribute(this.attrName_);
    else
      this.element_.setAttribute(this.attrName_, this.oldValue_);
  }

  SetAttribute.prototype = Object.create(editing.Operation.prototype, {
    constructor: SetAttribute,
    attrName_: {writable: true},
    element_: {writable: true},
    newValue_: {writable: true},
    oldValue_: {writable: true},
    redo: {value: redo},
    undo: {value: undo}
  });

  return SetAttribute;
})());

//////////////////////////////////////////////////////////////////////
//
// SetStyle
//
editing.define('SetStyle', (function() {
  /**
   * @constructor
   * @final
   * @param {!Element} element
   * @param {string} propertyName
   * @param {string} newValue
   */
  function SetStyle(element, propertyName, newValue) {
    editing.Operation.call(this, 'setAttribute');
    this.element_ = element;
    this.propertyName_ = propertyName;
    this.newValue_ = newValue;
    this.oldValue_ = element.style[propertyName];
    Object.seal(this);
  }

  /**
   * @this {!SetStyle}
   */
  function redo() {
    this.element_.style[this.propertyName_] = this.newValue_;
  }

  /**
   * @this {!SetStyle}
   */
  function undo() {
    this.element_.style[this.propertyName_] = this.oldValue_;
  }

  SetStyle.prototype = Object.create(editing.Operation.prototype, {
    constructor: SetStyle,
    element_: {writable: true},
    newValue_: {writable: true},
    oldValue_: {writable: true},
    propertyName_: {writable: true},
    redo: {value: redo},
    undo: {value: undo}
  });

  return SetStyle;
})());

//////////////////////////////////////////////////////////////////////
//
// SplitText
//
editing.define('SplitText', (function() {
  /**
   * @constructor
   * @final
   * @param {!Text} textNode
   * @param {!Text} newNode
   */
  function SplitText(textNode, newNode) {
    editing.Operation.call(this, 'splitText');
    this.newNode_ = newNode;
    this.textNode_ = textNode;
    Object.seal(this);
  }

  /**
   * @this {!SplitText}
   */
  function redo() {
    var text = this.textNode_.nodeValue;
    this.textNode_.nodeValue = text.substr(
        0, text.length - this.newNode_.nodeValue.length);
    this.textNode_.parentNode.insertBefore(this.newNode_,
                                           this.textNode_.nextSibling);
  }

  /**
   * @this {!SplitText}
   */
  function undo() {
    this.textNode_.nodeValue += this.newNode_.nodeValue;
    this.newNode_.parentNode.removeChild(this.newNode_);
  }

  SplitText.prototype = Object.create(editing.Operation.prototype, {
    constructor: SplitText,
    newNode_: {writable: true},
    redo: {value: redo},
    textNode_: {writable: true},
    undo: {value: undo}
  });

  return SplitText;
})());
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.




editing.define('SelectionDirection', {
  ANCHOR_IS_START: 'ANCHOR_IS_START',
  FOCUS_IS_START: 'FOCUS_IS_START'
});

editing.define('ReadOnlySelection', (function() {
  /**
   * @constructor
   * @final
   * @param {!Node} anchorNode
   * @param {number} anchorOffset
   * @param {!Node} focusNode
   * @param {number} focusOffset
   * @param {SelectionDirection} direction
   */
  function ReadOnlySelection(anchorNode, anchorOffset, focusNode, focusOffset,
                             direction) {
    this.anchorNode_ = anchorNode;
    this.anchorOffset_ = anchorOffset;
    this.direction_ = direction;
    this.focusNode_ = focusNode;
    this.focusOffset_ = focusOffset;
    Object.seal(this);
  }

  /** @type {!Node} */
  ReadOnlySelection.prototype.anchorNode;

  /** @type {number} */
  ReadOnlySelection.prototype.anchorOffset;

  /** @type {!Node} */
  ReadOnlySelection.prototype.focusNode;

  /** @type {number} */
  ReadOnlySelection.prototype.focusOffset;

  /** @type {boolean} */
  ReadOnlySelection.prototype.isCaret;

  /** @type {boolean} */
  ReadOnlySelection.prototype.isEmpty;

  /**
   * @this {!ReadOnlySelection}
   * @return {!Node}
   */
  function endContainer() {
    console.assert(this.anchorNode_);
    return this.direction_ == editing.SelectionDirection.FOCUS_IS_START ?
        this.anchorNode_ : this.focusNode_;
  }

  /**
   * @this {!ReadOnlySelection}
   * @return {number}
   */
  function endOffset() {
    console.assert(this.anchorNode_);
    return this.direction_ == editing.SelectionDirection.FOCUS_IS_START ?
        this.anchorOffset_ : this.focusOffset_;
  }

  /**
   * @this {!ReadOnlySelection}
   * @return {boolean}
   */
  function isCaret() {
    if (this.isEmpty)
      return false;
    return this.anchorNode_ === this.focusNode_ &&
           this.anchorOffset_ == this.focusOffset_;
  }

  /**
   * @this {!ReadOnlySelection}
   * @return {boolean}
   */
  function isEmpty() {
    return !this.anchorNode_;
  }

  /**
   * @this {!ReadOnlySelection}
   * @return {boolean}
   */
  function isRange() {
    return !this.isEmpty && !this.isCaret;
  }

  /**
   * @this {!ReadOnlySelection}
   * @return {!Node}
   */
  function startContainer() {
    console.assert(this.anchorNode_);
    return this.direction_ == editing.SelectionDirection.ANCHOR_IS_START ?
        this.anchorNode_ : this.focusNode_;
  }

  /**
   * @this {!ReadOnlySelection}
   * @return {number}
   */
  function startOffset() {
    console.assert(this.anchorNode_);
    return this.direction_ == editing.SelectionDirection.ANCHOR_IS_START ?
        this.anchorOffset_ : this.focusOffset_;
  }

  Object.defineProperties(ReadOnlySelection.prototype, {
    anchorNode: {get: function () { return this.anchorNode_; }},
    anchorNode_: {writable: true},
    anchorOffset: {get: function () { return this.anchorOffset_; }},
    anchorOffset_: {writable: true},
    direction: {get: function() { return this.direction_; }},
    direction_: {writable: true},
    endContainer: {get: endContainer},
    endOffset: {get: endOffset},
    focusNode: {get: function () { return this.focusNode_; }},
    focusNode_: {writable: true},
    focusOffset: {get: function () { return this.focusOffset_; }},
    focusOffset_: {writable: true},
    isCaret: {get: isCaret},
    isEmpty: {get: isEmpty},
    isRange: {get: isRange},
    startContainer: {get: startContainer},
    startOffset: {get: startOffset},
  });
  return ReadOnlySelection;
})());
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.



editing.define('SelectionTracker', (function() {
  /** @enum {string} */
  var StartOrEnd = {
    END: 'END',
    START: 'START'
  };

  /** @enum {string} */
  var TrackingType = {
    // <b>foo|</b> node is "foo".
    // <b>foo</b>| node is "b"
    AFTER_NODE: 'AFTER_NODE',
    // <b>|</b> node is "b"
    BEFORE_ALL_CHILDREN: 'BEFORE_ALL_CHILDREN',
    // <b>|foo</b> node is "b"
    // |<b>foo</b> node is "b"
    NODE: 'NODE'
  };

  /**
   * @constructor
   * @final
   * @param {!Node} node
   * @param {number} offset
   */
  function NodeAndOffset(node, offset) {
    this.node = node;
    this.offset = offset;
    Object.seal(this);
  }

  Object.defineProperties(NodeAndOffset.prototype, {
    node: {writable: true},
    offset: {writable: true}
  });

  /**
   * @constructor
   * @final
   * @param {!Node} node
   * @param {number} offset
   * @param {!StartOrEnd} startOrEnd
   */
  function TrackablePosition(node, offset, startOrEnd) {
    console.assert(editing.nodes.isElement(node), 'node=' + node);
    console.assert(offset >= 0, node, offset);
    var maxOffset = editing.nodes.maxOffset(node);
    console.assert(offset <= maxOffset, node, offset);
    if (!node.hasChildNodes()) {
      this.type_ = TrackingType.BEFORE_ALL_CHILDREN;
      this.node_ = node;
    } else if (maxOffset == offset) {
      this.type_ = TrackingType.AFTER_NODE;
      this.node_ = node.lastChild;
    } else if (offset && startOrEnd == StartOrEnd.END) {
      this.type_ = TrackingType.AFTER_NODE;
      this.node_ = node.childNodes[offset - 1];
    } else {
      this.type_ = TrackingType.NODE;
      this.node_ = node.childNodes[offset];
    }
    console.assert(this.node_, this, node, offset, startOrEnd, maxOffset, maxOffset == offset);
    Object.seal(this);
  }

  /**
   * @type {!function(): !NodeAndOffset}
   */
  TrackablePosition.prototype.convertToNodeAndOffset;

  /**
   * @type {!function(!Node)}
   */
  TrackablePosition.prototype.willRemoveNode;

  /**
   * @type {!function(!Element)}
   */
  TrackablePosition.prototype.willUnwrapElement;

  Object.defineProperties(TrackablePosition.prototype, (function() {
    /**
     * @this {!TrackablePosition}
     * @return {!NodeAndOffset}
     */
    function convertToNodeAndOffset() {
      var node = this.node_;
      switch (this.type_) {
        case TrackingType.AFTER_NODE:
          return new NodeAndOffset(node.parentNode,
                                   editing.nodes.nodeIndex(node) + 1);
        case TrackingType.BEFORE_ALL_CHILDREN:
          return new NodeAndOffset(node, 0);
        case TrackingType.NODE:
          return new NodeAndOffset(node.parentNode,
                                   editing.nodes.nodeIndex(node));
        default:
          throw new Error('Bad TrackablePosition.type ' + this.type_);
      }
    }

    /**
     * @this {!TrackablePosition}
     * @param {!Node} node
     */
    function willRemoveNode(node) {
      if (this.node_ !== node &&
          !editing.nodes.isDescendantOf(this.node_, node)) {
        return;
      }
      var oldNode = this.node_;
      var oldType = this.type_;
      this.type_ = TrackingType.AFTER_NODE;
      this.node_ = editing.nodes.previousNode(oldNode);
      console.assert(this.node_, this, oldNode, oldType, node);
    }

    /**
     * @this {!TrackablePosition}
     * @param {!Element} element
     */
    function willUnwrapElement(element) {
      if (this.node_ !== element)
        return;
      var oldNode = this.node_;
      var oldType = this.type_;
      switch (oldType) {
        case TrackingType.AFTER_NODE:
          this.type_ = TrackingType.NODE;
          this.node_ = editing.nodes.nextNodeSkippingChildren(oldNode);
          if (!this.node_) {
            this.type_ = TrackingType.AFTER_NODE;
            this.node_ = oldNode.lastChild;
          }
          break;
        case TrackingType.BEFORE_ALL_CHILDREN:
          throw new Error('UnwrapElement should not call for element without' +
                          ' children.');
          break;
        case TrackingType.NODE:
          this.type_ = TrackingType.NODE;
          this.node_ = oldNode.firstChild;
          break;
        default:
          throw new Error('Bad TrackablePosition.type ' + this.type_);
      }
      console.assert(this.node_, this, oldNode, oldType, element.outerHTML);
    }

    return {
      convertToNodeAndOffset: {value: convertToNodeAndOffset},
      node_: {writable: true},
      type_: {writable: true},
      willRemoveNode: {value: willRemoveNode},
      willUnwrapElement: {value: willUnwrapElement}
    }
  })());

  /**
   * @constructor
   * @final
   * @param {!EditingContext} context
   * @param {!ReadOnlySelection} selection
   */
  function SelectionTracker(context, selection) {
    this.context_ = context;
    this.start_ = new TrackablePosition(selection.startContainer,
                                        selection.startOffset,
                                        StartOrEnd.START);
    this.end_ = new TrackablePosition(selection.endContainer,
                                      selection.endOffset,
                                      StartOrEnd.END);
    Object.seal(this);
  }

  /**
   * @this {!SelectionTracker}
   */
  function finish() {
    var anchorNodeAndOffset;
    var focusNodeAndOffset;
    var selection = this.context_.startingSelection;
    if (selection.direction == editing.SelectionDirection.ANCHOR_IS_START) {
      anchorNodeAndOffset = this.start_.convertToNodeAndOffset();
      focusNodeAndOffset = this.end_.convertToNodeAndOffset();
    } else {
      focusNodeAndOffset = this.start_.convertToNodeAndOffset();
      anchorNodeAndOffset = this.end_.convertToNodeAndOffset();
    }
    this.context_.setEndingSelection(new editing.ReadOnlySelection(
        anchorNodeAndOffset.node, anchorNodeAndOffset.offset,
        focusNodeAndOffset.node, focusNodeAndOffset.offset,
        selection.direction));
  }

  /**
   * @this {!SelectionTracker}
   * @param {!Node} node
   */
  function willRemoveNode(node) {
    this.start_.willRemoveNode(node);
    this.end_.willRemoveNode(node);
  }

  /**
   * @this {!SelectionTracker}
   * @param {!Element} element
   *
   * This function is called before moving all children of |element| to
   * before |element|, we called "unwrap". |element| must have at least one
   * child.
   *
   * See "unlink" command for example of usage.
   */
  function willUnwrapElement(element) {
    this.start_.willUnwrapElement(element);
    this.end_.willUnwrapElement(element);
  }

  Object.defineProperties(SelectionTracker.prototype, {
    constructor: {value: SelectionTracker},
    context_: {writable: true},
    end_:{writable: true},
    finish: {value: finish},
    start_: {writable: true},
    willRemoveNode: {value: willRemoveNode},
    willUnwrapElement: {value: willUnwrapElement}
  });

  return SelectionTracker;
})());
